#include <GameEngine/Graphics/Shaders/shader_program.h>


#ifdef GAMEENGINE_NAMESPACES
namespace GameEngine {
#endif


//========================================================


#ifndef DOXYGEN_IGNORE

//! Default constructor
GLSLShaderProgramData::GLSLShaderProgramData() : shader_program_(0) {
	// Generate a new shader program ID
	shader_program_ = glCreateProgram();
}

//! Destructor
GLSLShaderProgramData::~GLSLShaderProgramData() {
	// Desallocate shader program ID
	if ( shader_program_ ) glDeleteProgram(shader_program_);
}

#endif


//========================================================


GAMEENGINE_IMPLEMENT_RTTI(GLSLShaderProgram)
GAMEENGINE_IMPLEMENT_PTR_RESOURCE( GLSLShaderProgram::ShaderProgramResourceManager )

//! Default constructor
GLSLShaderProgram::GLSLShaderProgram() : GLSLShaderProgram::RESOURCE_TYPE(1,true,rtti()) {
	create_id();
}

//! Constructor
/*!
 @param vertex_source    A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param geometry_source  A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param fragment_source  A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param from_file        If set to true, the parameters source should contain a path to
                         a text file which contains the shader object source code. Otherwise,
				         parameter source should contain the source code itself
 @param link_immediately If set to true (the default), the shader program is linked as soon as the
                         vertex, geometry and fragment shader objects have been defined. Otherwise,
						 linking has to be done manually using the link() function.
 @param input_type       The type of primitives accepted by the geometry shader as an input
 @param output_type      The type of primitives output by the geometry shader
 @param vertices_count   The maximal number of vertices output by the geometry shader
 */
GLSLShaderProgram::GLSLShaderProgram(const char* vertex_source, const char* geometry_source, const char* fragment_source, bool from_file,
									 bool link_immediately, const GeometryShaderInputType& input_type,
									 const GeometryShaderOutputType& output_type, unsigned int vertices_count) : GLSLShaderProgram::RESOURCE_TYPE(1,true,rtti())
{
	create_id();
	set_shader(vertex_source, geometry_source, fragment_source, from_file,
				link_immediately, input_type, output_type, vertices_count);
}

//! Copy constructor
/*!
 @param rhs The shader object to copy
 */
GLSLShaderProgram::GLSLShaderProgram(const GLSLShaderProgram& rhs) : GLSLShaderProgram::RESOURCE_TYPE(rhs) {
	context_ = rtti();
}

//! Destructor
GLSLShaderProgram::~GLSLShaderProgram() {
	clean_id();
}

//! Assignment operator
/*!
 @param rhs The shader program to copy
 */
GLSLShaderProgram& GLSLShaderProgram::operator=(const GLSLShaderProgram& rhs) {
	if ( &rhs != this ) {
		ShaderProgramResourceManager* manager = (ShaderProgramResourceManager*)ShaderProgramResourceManager::instance();
		if ( id_ != nil ) manager->unreference_resource(*this);
		context_ = rtti();
		id_ = rhs.id_;
		if ( id_ != nil ) manager->reference_resource(*this);
	}
	return *this;
}

//! Setup the shader program
/*!
 @param vertex_source    A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param geometry_source  A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param fragment_source  A string holding either the source code of the vertex shader object,
                         or a path to a file which contains this source code, depending
				         on the from_file flag
 @param from_file        If set to true, the parameters source should contain a path to
                         a text file which contains the shader object source code. Otherwise,
				         parameter source should contain the source code itself
 @param link_immediately If set to true (the default), the shader program is linked as soon as the
                         vertex, geometry and fragment shader objects have been defined. Otherwise,
						 linking has to be done manually using the link() function.
 @param input_type       The type of primitives accepted by the geometry shader as an input
 @param output_type      The type of primitives output by the geometry shader
 @param vertices_count   The maximal number of vertices output by the geometry shader
 @return True if shader could be setup, false otherwise
 */
bool GLSLShaderProgram::set_shader(const char* vertex_source, const char* geometry_source, const char* fragment_source, bool from_file,
									 bool link_immediately, const GeometryShaderInputType& input_type,
									 const GeometryShaderOutputType& output_type, unsigned int vertices_count)
{
	// Create the shader if required
	if ( id_ == nil ) create_id();

	// Clear previous objects
	remove_all_objects();

	// Setup shader objects
	if ( vertex_source != nil ) {
		GLSLShaderObject vertex;
		if ( vertex.set_shader(vertex_source, GLSLShaderObject::Vertex, from_file) == false ) return false;
		if ( add_object(vertex) == false ) return false;
	}
	if ( geometry_source != nil ) {
		GLSLShaderObject geometry;
		if ( geometry.set_shader(geometry_source, GLSLShaderObject::Geometry, from_file) == false ) return false;
		if ( set_geometry_parameters(input_type, output_type, vertices_count) == false ) return false;
		if ( add_object(geometry) == false ) return false;
	}
	if ( fragment_source != nil ) {
		GLSLShaderObject fragment;
		if ( fragment.set_shader(fragment_source, GLSLShaderObject::Fragment, from_file) == false ) return false;
		if ( add_object(fragment) == false ) return false;
	}

	// Link program if required
	if ( link_immediately == false ) return true;
	return link();
}

//! Setup the geometry shader parameters
/*!
 @param input_type     The type of primitives accepted by the geometry shader as an input
 @param output_type    The type of primitives output by the geometry shader
 @param vertices_count The maximal number of vertices output by the geometry shader
 @return True if shader could be setup, false otherwise
 */
bool GLSLShaderProgram::set_geometry_parameters(const GeometryShaderInputType& input_type, const GeometryShaderOutputType& output_type,
												unsigned int vertices_count)
{
	glProgramParameteriEXT(id_->shader_program_,GL_GEOMETRY_INPUT_TYPE_EXT,input_type);
	glProgramParameteriEXT(id_->shader_program_,GL_GEOMETRY_OUTPUT_TYPE_EXT,output_type);
	glProgramParameteriEXT(id_->shader_program_,GL_GEOMETRY_VERTICES_OUT_EXT,vertices_count);
	return true;
}


//! Activate the shader
/*!
 @note The shader will be in use until the disable() function
       is called
 */
void GLSLShaderProgram::enable() const {
	glUseProgram(id_->shader_program_);
}


//! Desactivate the shader
/*!
 @note After a call of this function, all graphic primitives
       will be drawn using the OpenGL fixed-functionalities
 */
void GLSLShaderProgram::disable() const {
	glUseProgram(0);
}

//! Try to add a shader object to the shader program
/*!
 *  If shader object is actually added, this function also attaches the shader
 *  object to the shader program. Please note that to truely change the shader
 *  program, the link() function has to be called after the object has been
 *  added.
 *
 @param object The shader object to add to this shader program
 @return True if shader object could be added, false otherwise
 */
bool GLSLShaderProgram::add_object(const GLSLShaderObject& object) {
	// Try to find the shader object in the list
	if ( find_object(object) == true ) return false;

	// Add the object and attach it to the program
	id_->objects_.push_back(object);
	glAttachShader(id_->shader_program_, object.shader_object_);
	return true;
}

//! Try to find a shader object to the shader program
/*!
 @param object The shader object to find in this shader program
 @return True if shader object could be found, false otherwise
 */
bool GLSLShaderProgram::find_object(const GLSLShaderObject& object) const {
	// Iterate over the objects of the program
	for ( std::vector< GLSLShaderObject >::const_iterator it = id_->objects_.begin(); it != id_->objects_.end(); ++it ) {
		if ( (*it) == object ) return true;
	}

	// Shader object could not be found
	return false;
}

//! Try to remove a shader object from the shader program
/*!
 *  If shader object is actually removed, this function also detaches the shader
 *  object from the shader program. Please note that to truely change the shader
 *  program, the link() function has to be called after the object has been
 *  removed.
 *
 @param object The shader object to remove from this shader program
 @return True if shader object could be removed, false otherwise
 */
bool GLSLShaderProgram::remove_object(const GLSLShaderObject& object) {
	// Iterate over the objects of the program
	for ( std::vector< GLSLShaderObject >::iterator it = id_->objects_.begin(); it != id_->objects_.end(); ++it ) {
		// Test shader object
		if ( (*it) != object ) continue;

		// Remove and detach the shader object
		glDetachShader(id_->shader_program_, it->shader_object_);
		id_->objects_.erase(it);
		return true;
	}

	// Shader object could not be found
	return false;
}

//! Remove all shader objects from the shader program
/*!
 *  This function also detaches the shader objects from the shader program. Please
 *  note that to truely change the shader program, the link() function has to be
 *  called after the objects have been removed.
 *
 @return True if shader objects could be removed, false otherwise
 */
bool GLSLShaderProgram::remove_all_objects() {
	// Check that there are objects to remove
	if ( id_->objects_.empty() == true ) return false;

	// Iterate over the objects of the program
	for ( std::vector< GLSLShaderObject >::iterator it = id_->objects_.begin(); it != id_->objects_.end(); ++it ) {
		// Remove and detach the shader object
		glDetachShader(id_->shader_program_, it->shader_object_);
	}
	id_->objects_.clear();
	return true;
}

//! Find the identifier of a vertex attribute variable
/*!
 @param name The name of the attribute variable
 @return The ID of the shader attribute variable (-1 if no such variable
         could be found)
 */
GLint GLSLShaderProgram::get_attribute(const char* name) const {
	return glGetAttribLocation(id_->shader_program_, name);
}

//! Find the identifier of a uniform variable
/*!
 @param name The name of the uniform variable
 @return The ID of the shader uniform variable (-1 if no such variable
         could be found)
 */
GLint GLSLShaderProgram::get_uniform(const char* name) const {
	return glGetUniformLocation(id_->shader_program_, name);
}

//! Sets the value of a shader uniform variable of int type
/*!
 @param id Identifier of the uniform variable, obtained with the get_uniform() function
 @param value Value to set the uniform variable to
 */
void GLSLShaderProgram::uniform(GLint id, int value) const {
	CHECK_SHADER_ID(id)
	glUniform1i(id, value);
}

//! Sets the value of a shader uniform variable of float type
/*!
 @param id Identifier of the uniform variable, obtained with the get_uniform() function
 @param value Value to set the uniform variable to
 */
void GLSLShaderProgram::uniform(GLint id, float value) const {
	CHECK_SHADER_ID(id)
	glUniform1f(id, value);
}

//! Sets the value of a shader uniform variable of vec2 type
/*!
 @param id Identifier of the uniform variable, obtained with the get_uniform() function
 @param v0 Value to set the first component of the uniform variable to
 @param v1 Value to set the second component of the uniform variable to
 */
void GLSLShaderProgram::uniform2f(GLint id, float v0, float v1 ) const {
	CHECK_SHADER_ID(id)
	glUniform2f(id, v0,v1);
}

//! Sets the value of a shader uniform variable of vec3 type
/*!
 @param id Identifier of the uniform variable, obtained with the get_uniform() function
 @param v0 Value to set the first component of the uniform variable to
 @param v1 Value to set the second component of the uniform variable to
 @param v2 Value to set the third component of the uniform variable to
 */
void GLSLShaderProgram::uniform3f(GLint id, float v0, float v1, float v2 ) const {
	CHECK_SHADER_ID(id)
	glUniform3f(id, v0,v1,v2);
}

//! Sets the value of a shader uniform variable of vec4 type
/*!
 @param id Identifier of the uniform variable, obtained with the get_uniform() function
 @param v0 Value to set the first component of the uniform variable to
 @param v1 Value to set the second component of the uniform variable to
 @param v2 Value to set the third component of the uniform variable to
 @param v3 Value to set the fourth component of the uniform variable to
 */
void GLSLShaderProgram::uniform4f(GLint id, float v0, float v1, float v2, float v3 ) const {
	CHECK_SHADER_ID(id)
	glUniform4f(id, v0,v1,v2,v3);
}

//! Sets the value of a shader attribute variable of float type
/*!
 @param id Identifier of the attribute variable, obtained with the get_attribute() function
 @param value Value to set the attribute variable to
 */
void GLSLShaderProgram::attribute(GLint id, float value) const {
	CHECK_SHADER_ID(id)
	glVertexAttrib1f(id, value);
}

//! Sets the value of a shader attribute variable of vec2 type
/*!
 @param id Identifier of the attribute variable, obtained with the get_attribute() function
 @param v0 Value to set the first component of the attribute variable to
 @param v1 Value to set the second component of the attribute variable to
 */
void GLSLShaderProgram::attribute2f(GLint id, float v0, float v1 ) const {
	CHECK_SHADER_ID(id)
	glVertexAttrib2f(id, v0,v1);
}

//! Sets the value of a shader attribute variable of vec3 type
/*!
 @param id Identifier of the attribute variable, obtained with the get_attribute() function
 @param v0 Value to set the first component of the attribute variable to
 @param v1 Value to set the second component of the attribute variable to
 @param v2 Value to set the third component of the attribute variable to
 */
void GLSLShaderProgram::attribute3f(GLint id, float v0, float v1, float v2 ) const {
	CHECK_SHADER_ID(id)
	glVertexAttrib3f(id, v0,v1,v2);
}

//! Sets the value of a shader attribute variable of vec4 type
/*!
 @param id Identifier of the attribute variable, obtained with the get_attribute() function
 @param v0 Value to set the first component of the attribute variable to
 @param v1 Value to set the second component of the attribute variable to
 @param v2 Value to set the third component of the attribute variable to
 @param v3 Value to set the fourth component of the attribute variable to
 */
void GLSLShaderProgram::attribute4f(GLint id, float v0, float v1, float v2, float v3 ) const {
	CHECK_SHADER_ID(id)
	glVertexAttrib4f(id, v0,v1,v2,v3);
}

//! Try to link the shader program
/*!
 @return True if linking succeeded, false otherwise
 */
bool GLSLShaderProgram::link() const {
	// Try to link the shader program
	if ( id_->shader_program_ == 0 ) return false;
	glLinkProgram(id_->shader_program_);

	// Check compilation
	return check_linking("Shader error - cannot compile.\n");
}

//! Check whether the shader program was correctly linked
/*!
 @param message The message to be output in case of error
 @return True if linking was successful, false otherwise
 */
bool GLSLShaderProgram::check_linking(const char* message) const {
	// Check link status
    int success;
    glGetProgramiv(id_->shader_program_, GL_LINK_STATUS, &success);
	if ( success != GL_FALSE ) return true;

	// Print custom error message
	std::cerr << message << std::endl;

	// Get error log
    int infoLogSize;
    glGetProgramiv(id_->shader_program_, GL_INFO_LOG_LENGTH, &infoLogSize);
    char* buffer = MemoryManager::instance()->allocate<char>(infoLogSize);
	glGetProgramInfoLog(id_->shader_program_, infoLogSize, NULL, buffer);

	// Print error log
	std::cerr << buffer << std::endl;
	MemoryManager::instance()->desallocate(buffer,infoLogSize);

	// Return failure
	return false;
}

//! Return the shader program context
GLSLShaderProgram::CONTEXTID GLSLShaderProgram::context() const {
	return rtti_id_;
}

//! Check if the glew library is correctly initialized
/*!
 @return False if glew cannot be initialized, true otherwise
 */
bool GLSLShaderProgram::check_glew() {
	// Check the create program function. If it is set to nil, glew is either not yet initialized or unable to run shaders
	if ( glCreateProgram == NULL ) {
		// Try to initialize glew
		glewInit();

		// Check compatibility
		if ( GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader && GLEW_GET_VAR(__GLEW_EXT_geometry_shader4) /* && GL_EXT_geometry_shader4 */ ) {
			std::cout << "GPU compatible with GLSL - vertex, fragment, and geometry units." << std::endl;
		}
		else if ( GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader ) {
			std::cout << "GPU compatible with GLSL - vertex and fragment units." << std::endl;
			std::cout << "GPU not compatible with GLSL - geometry unit." << std::endl;
		}
		else {
			std::cerr << "GPU cannot run shaders." << std::endl;
		}

		// Check glew initialization
		if ( glCreateProgram == NULL ) {
			std::cerr << "Cannot initialize glew for GLSL shaders." << std::endl;
			return false;
		}
	}

	return true;
}

#ifdef GAMEENGINE_NAMESPACES
} // namespace GameEngine
#endif
